52.在vue中使用d3创建力导向图并动态新增、删除节点及联系 您所在的位置:网站首页 从当前 url 追加删除锚名称而不刷新 52.在vue中使用d3创建力导向图并动态新增、删除节点及联系

52.在vue中使用d3创建力导向图并动态新增、删除节点及联系

2023-03-27 12:55| 来源: 网络整理| 查看: 265

0.写在前面

要实现的功能如图:

1.安装d3 npm install d3 --save-dev 2.在页面中引入d3 import * as d3 from 'd3' 3.在页面中增加热词tag和类名为container的div元素 热词图谱: {{ item }} 生成结论 4.初始化力导向图

(1)调接口获取nodes和links后走initGraph方法

async getKeyWord(data) { const response = await get_keyword(data) this.testGraph['nodes'] = response.data.nodes this.testGraph['links'] = response.data.links this.initGraph(this.testGraph) },

(2)初始化前先清除上次的图谱 获取的数据存成全局变量

由于要注解,就不把所有代码一次性粘贴上了

如下图,在initGraph方法里,通过d3.select('#the_SVG_ID').remove() 清除上次的图谱;

由于更新图谱时需要往nodes和links数组里添加节点及联系,存成全局变量方便在其他方法里使用。这里需要引入Vue;

import Vue from 'vue' // 存入全局变量 Vue.prototype.$links = links Vue.prototype.$nodes = nodes // 引用全局变量 that.$links that.$nodes

下边3块代码分别是添加碰撞力和引力及控制link的长度、在container元素中创建svg元素用来放节点和连线、缩放;

 这里应该会注意到 that ,这个that使用的是全局变量,因为点击node节点的箭头函数里this不生效,所以设置了that代替this:

initGraph(data) { d3.select('#the_SVG_ID').remove() const links = data.links.map(d => Object.create(d)) const nodes = data.nodes.map(d => Object.create(d)) Vue.prototype.$links = links Vue.prototype.$nodes = nodes // .distance(160)) 改变link的长度 that.simulation = d3.forceSimulation(that.$nodes) .force('link', d3.forceLink(links).id(d => d.keyword).distance(150)) .force('collide', d3.forceCollide().radius(() => 90)) // 碰撞力 .force('charge', d3.forceManyBody().strength(-90)) // 引力 .force('center', d3.forceCenter(that.width / 2, that.height / 2)); // 创建svg元素 初始化样式 const svg = d3.select('.container') .append('svg') .attr('id', 'the_SVG_ID') .attr('viewBox', [0, 0, that.width, that.height]) .style('width', 1600) .style('height', 800) // 缩放 svg.call(d3.zoom().on('zoom', function() { g.attr('transform', d3.event.transform) })) }

(3)设置箭头控制方向、svg元素中创建g元素并向其中加入svg_links和linksName

// 两个marker控制箭头方向 stroke-width 箭头粗细 refX 偏移 orient 朝向 const positiveMarker = svg.append('marker') .attr('id', 'positiveMarker') .attr('orient', 'auto') .attr('stroke-width', 2) .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('refX', 26) .attr('refY', 0) .attr('markerWidth', 12) .attr('markerHeight', 12) .append('path') .attr('d', 'M 0 -5 L 10 0 L 0 5') .attr('fill', '#999') .attr('stroke-opacity', 0.6) const negativeMarker = svg.append('marker') .attr('id', 'negativeMarker') .attr('orient', 'auto') .attr('stroke-width', 2) .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('refX', -16) .attr('refY', 0) .attr('markerWidth', 12) .attr('markerHeight', 12) .append('path') .attr('d', 'M 10 -5 L 0 0 L 10 5') .attr('fill', '#999') .attr('stroke-opacity', 0.6) // 在svg中创建g元素 将node和link放在g元素中 更精确 const g = svg.append('g') // .attr("marker-end","url(#direction)") 添加箭头 that.svg_links = g.append('g') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .attr('marker-end', 'url(#direction)') .selectAll('path') .data(that.$links) .join('path') .attr('stroke-width', d => Math.sqrt(d.value)) .attr('id', function(d) { if (typeof (d.source) === 'object') { return d.source.keyword + '_' + d.relationship + '_' + d.target.keyword } else { return d.source + '_' + d.relationship + '_' + d.target } }) // linksName 连线上的文字 text-anchor 锚点 startOffset 开始偏移 这两个属性实现居中 that.linksName = g.append('g') .selectAll('text') .data(that.$links) .join('text') // .attr('x', 70) // .attr('y', 60) .style('text-anchor', 'middle') .style('fill', '#595959') .style('font-size', '12px') .style('font-weight', 'bold') .append('textPath') .attr( 'xlink:href', function(d) { if (typeof (d.source) === 'object') { return '#' + d.source.keyword + '_' + d.relationship + '_' + d.target.keyword } else { return '#' + d.source + '_' + d.relationship + '_' + d.target } } ) .attr('startOffset', '50%') // .attr('dx', 10) // .attr('dy', 10) .text(function(d) { if (d.count) { return '数量:' + d.count } else { return '数量:' + 1 } })

(4)向g元素中添加svg_nodes 点击节点时向热词列表添加热词并调接口获取该节点相关图谱

如下图:点击节点时,先判断热词列表里有没有该节点,并且group为2(评论)也不添加;

获取的新nodes和links肯定有重复的,所以需要判断是否重复;

最后添加到全局变量that.$nodes和that.$links中,并调updateGraph方法动态更新图谱

that.svg_nodes = g.append('g') .attr('stroke', '#fff') .attr('stroke-width', 1.5) .selectAll('circle') .data(that.$nodes) .join('circle') .attr('r', function(d) { if (d.group === 2) { return 25 } else { return 20 } }) .attr('class', 'node') .attr('fill', that.color) // 点击元素获取对应信息 .on('click', function(d, i) { that.isTrue = false that.wordData = [] that.clickWord.map((item,index) =>{ if(d.group === 2){ return }else if(that.clickWord.indexOf(d.keyword) != -1){ return }else{ that.clickWord.push(d.keyword) } }) // 根据人名d.keyword 查询到对应的link联系 const data = { 'word': d.keyword, 'start_time': that.start_time, 'end_time': that.end_time } get_keyword(data) .then(function(res) { if (res.status) { res.data.nodes.map(item => { let flag = true for (var j = 0; j < that.$nodes.length; j++) { if (that.$nodes[j].keyword === item.keyword) { flag = false break } } if (flag) { that.$nodes.push(item) } }) res.data.links.map(item1 => { let flag = true for (var j = 0; j < that.$links.length; j++) { if (that.$links[j].target.keyword === item1.source === d.keyword) { that.$links.splice(j, 1) } if (that.$links[j].source.keyword === item1.source) { flag = false break } } if (flag) { that.$links.push(item1) } }) that.updateGraph(d.keyword) } }) .catch(function(err) { console.log(err) }) }) .call(that.drag(that.simulation)) that.svg_nodes.append('title') .text(function(d) { return d.keyword })

(5)向g元素中添加节点名称nodesName 设置力图布局

设置力图布局这里就用到了两个箭头常量:#positiveMarker   #negativeMarker

// nodesName title显示在node下方 that.nodesName = g.append('g') .selectAll('text') .data(that.$nodes) .join('text') .text(function(d) { if (d.keyword.length > 2) { return d.keyword.slice(0, 2) + '...' } else { return d.keyword } }) // .attr('dx', function() { // return this.getBoundingClientRect().width / 2 * (-1) // }) .attr('dx', -15) .attr('dy', 10) .attr('class', 'nodeName') // 力图布局 that.simulation.on('tick', () => { that.svg_links .attr('d', function(d) { if (d.source.x < d.target.x) { return 'M' + d.source.x + ' ' + d.source.y + 'L' + d.target.x + ' ' + d.target.y } else { return 'M' + d.target.x + ' ' + d.target.y + 'L' + d.source.x + ' ' + d.source.y } }) .attr('marker-end', function(d) { if (d.source.x < d.target.x) { return 'url(#positiveMarker)' } else { return null } }) .attr('marker-start', function(d) { if (d.source.x < d.target.x) { return null } else { return 'url(#negativeMarker)' } }) that.svg_nodes .attr('cx', d => d.x) .attr('cy', d => d.y) that.nodesName .attr('x', d => d.x) .attr('y', d => d.y) }) 5.动态更新图谱方法

在点击图谱中的某个热词时,会先调接口获取与之相关的热词及联系,然后再调这个updateGraph方法

(1)遍历节点将点击节点改变填充颜色 遍历连线改变箭头方向

如下图:在这个更新图谱方法中,首先先去遍历所有节点,截取其id,判断哪个包含点击的热词,将其填充颜色改成绿色;

上步完成,就是遍历所有连线,将之前的评论与你点击的热词间的连线删掉,因为新的关系出来,连线箭头会改变

updateGraph(keyword){ var sel = d3.select(that.svg_nodes)._groups[0][0]._groups[0] sel.map((item,index) =>{ let tempArr = [] tempArr = item.innerHTML.split('') const newStr = tempArr.join('') let tempArr1 = [] tempArr1 = newStr.split('') const newStr1 = tempArr1.join('') if(newStr1.length < 20 && item.__data__.group == 1){ if(newStr1.indexOf(keyword) != -1){ sel[index].style.fill = '#82E0AA' } } }) that.svg_links._groups[0].map((item,index) =>{ if(item.id.slice(0,15).indexOf('app_keyword') != -1){ return }else{ let uid = item.id.substring(item.id.length - 5) if(uid.indexOf(keyword) != -1){ d3.select(that.svg_links._groups[0][index]).remove() } } }) }

(2)向初始节点数组中添加新节点 点击节点再次调用更新图谱方法

 如下图:在点击节点的方法中,先遍历所有连线,判断点击的节点是否不是之前评论相连的节点,并且是新的评论相连节点

如果两个条件都满足,再做一下限制,之前点过的节点和group为2即为评论的节点都不能再点击

最后和初始化里一样,先调接口获取点击的节点相关联的热词和联系,再调updateGraph方法

that.svg_nodes = that.svg_nodes .data(that.$nodes) .enter() .append('circle') .attr('r', function(d) { if (d.group === 2) { return 25 } else { return 20 } }) .attr('fill', that.color) .attr('class', 'node') .merge(that.svg_nodes) .on('click', function(d, i) { for(var i=0;i{ if(d.group === 2){ return }else if(that.clickWord.indexOf(d.keyword) != -1){ return }else{ that.clickWord.push(d.keyword) } }) // 根据人名d.keyword 查询到对应的link联系 const data = { 'word': d.keyword, 'start_time': that.start_time, 'end_time': that.end_time } get_keyword(data) .then(function(res) { if (res.status) { res.data.nodes.map(item => { let flag = true for (var j = 0; j < that.$nodes.length; j++) { if (that.$nodes[j].keyword === item.keyword) { flag = false break } } if (flag) { that.$nodes.push(item) } }) res.data.links.map(item1 => { let flag = true for (var j = 0; j < that.$links.length; j++) { if (that.$links[j].target.keyword === item1.source === d.keyword) { that.$links.splice(j, 1) } if (that.$links[j].source.keyword === item1.source) { flag = false break } } if (flag) { that.$links.push(item1) } }) that.updateGraph(d.keyword) } }) .catch(function(err) { console.log(err) }) } } }) .call(that.drag(that.simulation))

(3)添加新节点名称、新连线、新连线名称 并重新启动simulation

that.svg_nodes.append('title') .text(function(d) { return d.keyword }) // nodesName title显示在node下方 that.nodesName = that.nodesName .data(that.$nodes) .enter() .append('text') .merge(that.nodesName) .text(function(d) { if (d.keyword.length > 2) { return d.keyword.slice(0, 2) + '...' } else { return d.keyword } }) .attr('dx', -10) .attr('dy', 8) .attr('class', 'nodeName') that.svg_links = that.svg_links .data(that.$links) .enter() .append('path') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .attr('stroke-width', d => Math.sqrt(d.value)) .attr('marker-end', 'url(#direction)') .attr('id', function(d) { if (typeof (d.source) === 'object') { return d.source.keyword + '_' + d.relationship + '_' + d.target.keyword } else { return d.source + '_' + d.relationship + '_' + d.target } }) .merge(that.svg_links) // linksName 连线上文字 that.linksName = that.linksName .data(that.$links) .enter() .append('text') .style('text-anchor', 'middle') .style('fill', 'black') .style('font-size', '10px') .style('font-weight', 'bold') .append('textPath') .attr( 'xlink:href', function(d) { if (typeof (d.source) === 'object') { return '#' + d.source.keyword + '_' + d.relationship + '_' + d.target.keyword } else { return '#' + d.source + '_' + d.relationship + '_' + d.target } } ) .attr('startOffset', '50%') .merge(that.linksName) .text(function(d) { if (d.count) { return '数量:' + d.count } else { return '数量:' + 1 } }) that.simulation.nodes(that.$nodes) that.simulation.force('link').links(that.$links) that.simulation.alpha(1).restart() 6.删除选词

这里根据需求,设置的是当删除的是第一个选词的话,会清空图谱,删除其他的再去判断

如下图:在删除其他选词时,会先循环选词数组,如果删除的选词与数组的某一个相同则删除,

然后遍历links数组,如果某个连线的id前几个词包含选词数组某个词则也跟着删除;

这里的意思就是,当删除的那个词后边的词是由你删除的词散开的词,则跟着删除,否则不删

最后就是遍历选词数组剩余的词,如果是第一个还走 initGraph 方法;否则走 updateGraph 方法

// 删除选词结果 delWord(val) { if (val == that.clickWord[0]) { d3.select('#the_SVG_ID').remove() that.clickWord = [] that.isTrue = false that.wordValue = '' that.wordData = [] that.wordFuzzyData = [] }else{ for(var i=1;i{ if(typeof(that.clickWord[i]) != 'undefined'){ let uid = '' if(that.clickWord[i].length > 2){ uid = item.id.substring(0,4) }else{ uid = item.id.substring(0,2) } if(uid.indexOf(that.clickWord[i]) != -1){ that.clickWord.splice(i,1) } } }) } } } that.clickWord.map((item,index) =>{ if(index == 0){ const data = { 'word': item, 'start_time': this.start_time, 'end_time': this.end_time } this.getKeyWord(data) } if(index != 0){ const data = { 'word': item, 'start_time': that.start_time, 'end_time': that.end_time } get_graphdata_app_keyword(data) .then(function(res) { if (res.status) { res.data.nodes.map(item2 => { let flag = true for (var j = 0; j < that.$nodes.length; j++) { if (that.$nodes[j].keyword === item2.keyword) { flag = false break } } if (flag) { that.$nodes.push(item2) } }) res.data.links.map(item3 => { let flag = true for (var j = 0; j < that.$links.length; j++) { if (that.$links[j].target.keyword === item3.source === item) { that.$links.splice(j, 1) } if (that.$links[j].source.keyword === item3.source) { flag = false break } } if (flag) { that.$links.push(item3) } }) that.updateGraph(item) } }) .catch(function(err) { console.log(err) }) } }) },

(本文完)



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有